import java.util.LinkedList;


import umontreal.iro.lecuyer.simevents.Accumulate;
import umontreal.iro.lecuyer.simevents.Event;
import umontreal.iro.lecuyer.simevents.Sim;
import umontreal.iro.lecuyer.simprocs.*;
import umontreal.iro.lecuyer.charts.XYLineChart;
import umontreal.iro.lecuyer.rng.*;
import umontreal.iro.lecuyer.stat.*;

public class GasStation {
	
	//The simulator engine
	ProcessSimulator sim = new ThreadProcessSimulator();
	
	
	//type
	class Car { double enterTime, servTime, leaveTime; } 
	
	
	
	/// PARAMETERS  (and linked variable)
	
	//Size of the queue
	int wait_queue_size;
	
	//List of the waiting client
	LinkedList<Car> waitQueue = new LinkedList<Car> ();
	
	//List of car using the pump, associated with the pumps array
	Car usingPump[];
	
	//Pump number
	int pump_nbr;
	
	//Pump
	Resource pumps[];
	
	//Simulation duration, in minutes
	double duration;
	
	//Order of pumps liberation
	LinkedList<Integer> orderPumpFree = new LinkedList<Integer> ();
	
	//Minimum time to refuel
	double min_time_refuel;
	
	//Maximum duration of a refuel
	double max_time_refuel;
	
	
	//RANDOM MANAGEMENT STUFF
	//Random generator for arrival
	RandomStream car_arr_rand = new MRG32k3a(); //TO DO
	
	//Random generator for using the pump
	RandomStream pump_time_rand = new MRG32k3a(); //TO DO
	
	
	///Reource management function
	
	private boolean resource_available()
	{
		for(int i=0; i<pump_nbr; i++)
		{
			if(pumps[i].getAvailable() > 0)
			{return true; }
		}
		
		return false;
	}
	
	private int get_next_available_resource()
	{
		for(int i=0; i<pump_nbr; i++)
		{
			if(pumps[i].getAvailable() > 0)
			{ return i; }
		}
		
		return -1;
	}
	
	private int number_resource_available()
	{
		int ret = 0;
		for(int i=0; i<pump_nbr; i++)
		{
			if(pumps[i].getAvailable() > 0)
			{ ret++; }
		}
		return ret;
	}
	
	
	///RESULTS (to analyze)
	
	//Number of served client (not missed)
	int clientNbr;
	Accumulate NbClientsAcc = new Accumulate ("Nb. client served");
	static double nbClientArray[][];
	
	//Number of served client (not missed)
	int clientMissedNbr;
	Accumulate NbClientsMissedAcc = new Accumulate ("Nb. client missed");
	static double nbClientMissedArray[][];
	
	//Waiting time in the queue
	TallyStore waitingQueueTimeTally = new TallyStore ("Time passed in the queue"); 
	static double waitingQueueArray[][];
	
	//Time spent in the station
	TallyStore servingTimeTally = new TallyStore ("Total time spend in the station");
	static double timeInStationArray[][];
	
	//Size of the waiting queue
	Tally waitingQueueSizeTally = new Tally ("Size of the waiting queue");
	static double waitingQueueSizeArray[][];
	
	//Number of pumps used
	Tally pumpInUsageTally = new Tally ("Numner of used pump");
	static double pumpInUsageArray[][];
	
	
	///Events
	
	//Incoming new car !
	 class Arrival extends Event {
	      public void actions() {
	    	  Car newCar = new Car();
	    	  newCar.enterTime = Sim.time();
	    	  //Next arrival
	    	  new Arrival().schedule ( schedule_next_arrival(Sim.time()) );
	    	  
	    	  //If queue is full, miss this client
	    	  if( waitQueue.size() >= wait_queue_size)
	    	  {
	    		  clientMissedNbr++;
	    		  NbClientsMissedAcc.update(clientMissedNbr);	    		  
	    		  nbClientMissedArray[0][(int) Sim.time()] = Sim.time();
	    		  nbClientMissedArray[1][(int) Sim.time()] = clientMissedNbr;
	    	  }
	    	  //put it in the queue
	    	  else 
	    	  {
	    		  waitQueue.add(newCar);
	    		  //Update nbr client entered
	    		  clientNbr++;
	    		  NbClientsAcc.update(clientNbr);
	    		  nbClientArray[0][(int) Sim.time()] = Sim.time();
	    		  nbClientArray[1][(int) Sim.time()] = clientNbr;
	    		  
	    		  //Update size of the queue
	    		  waitingQueueSizeTally.add(waitQueue.size());
	    		  waitingQueueSizeArray[0][(int) Sim.time()] = Sim.time();
	    		  waitingQueueSizeArray[1][(int) Sim.time()] = waitQueue.size();
	    		  
	    		  //If the queue is empty and a pump is available -> leave and available
	    		  //First condition is to avoid having the same event several times
		          if( waitQueue.size() == 1 && resource_available() )
		    	  {		    		  
		    		  new LeaveQueue().schedule (0.0);
		    	  }	    	
	    	  }
	      }
	      
	      //Provide the time when the next car will come
	      //distribution law
	      private double  schedule_next_arrival(double sim_time)
	      {
	    	  double time_of_day = sim_time % 1440;
	    	  double min;
	    	  double max;
	    	  //use a dist ?
	    	  //double next_arr = PoissonDist.cdf( 1020 ,(int) time_of_day); //17h
	    	
	    	  if( time_of_day/60 < 7)
	    	  {
	    		  min = 10;
	    		  max = 100;
	    	  }
	    	  else if( time_of_day/60 < 10)
	    	  {
	    		  min = 5;
	    		  max = 15;
	    	  }
	    	  else if( time_of_day/60 < 15)
	    	  {
	    		  min = 1;
	    		  max = 10;
	    	  }
	    	  else if( time_of_day/60 < 18)
	    	  {
	    		  min = 0;
	    		  max = 3;
	    	  }
	    	  else if( time_of_day/60 < 20)
	    	  {
	    		  min = 1;
	    		  max = 10;
	    	  }
	    	  else
	    	  {
	    		  min = 5;
	    		  max = 30;
	    	  } 
	    	  
	    	  return car_arr_rand.nextDouble() *(max-min) + min;
	      }
	 }
	 
	 ///Leaving the queue
	 class LeaveQueue extends Event {
	      public void actions() {
	    	 
	    	  if( waitQueue.size() > 0 )
	    	  {
	    		  int res_pos = get_next_available_resource();
	    		  
	    		  if(res_pos != -1)
	    		  {
	    			  //Locking the pump
	    			  pumps[res_pos].request(1);
	    			  //Car leaving the queue
	    			  Car toGoToPump = waitQueue.poll();
	    			  toGoToPump.servTime = Sim.time();
	    			  
	    			  //Update result
	    			  waitingQueueTimeTally.add(toGoToPump.servTime - toGoToPump.enterTime);
	    			  waitingQueueArray[0][(int) Sim.time()] = Sim.time();
	    			  waitingQueueArray[1][(int) Sim.time()] = toGoToPump.servTime - toGoToPump.enterTime;
	    			  
		    		  //Update size of the queue
		    		  waitingQueueSizeTally.add(waitQueue.size());
		    		  waitingQueueSizeArray[0][(int) Sim.time()] = Sim.time();
		    		  waitingQueueSizeArray[1][(int) Sim.time()] = waitQueue.size();
		    		  
		    		  //Updating pump number
			    	  pumpInUsageTally.add( pump_nbr - number_resource_available());
			    	  pumpInUsageArray[0][(int) Sim.time()] = Sim.time();
			    	  pumpInUsageArray[1][(int) Sim.time()] = pump_nbr - number_resource_available();
	    			  
	    			  //Add it to the array
	    			  usingPump[res_pos] = toGoToPump;
	    			  
	    			  //Call event
	    			  orderPumpFree.add(res_pos);	    			  
	    			  //time to serve...
	    			  new Departure().schedule ( pump_time_rand.nextDouble() *(max_time_refuel-min_time_refuel) + min_time_refuel);
	    		  }
	    	  }
	      }
	 }
	 
	 ///Leaving the station (having just finished refueling)
	 class Departure extends Event {
	      public void actions() {
	    	  //Get the pump liberated
	    	  int res_pos = orderPumpFree.poll();
	    	  
	    	  pumps[res_pos].release(1);
	    	  
	    	  //Updating pump number
	    	  pumpInUsageTally.add( pump_nbr - number_resource_available());
	    	  pumpInUsageArray[0][(int) Sim.time()] = Sim.time();
	    	  pumpInUsageArray[1][(int) Sim.time()] = pump_nbr - number_resource_available();
	    	  
	    	  //Get the car
	    	  Car leavingCar = usingPump[res_pos];
	    	  leavingCar.leaveTime = Sim.time();
	    	  servingTimeTally.add(leavingCar.leaveTime - leavingCar.enterTime);
	    	  timeInStationArray[0][(int) Sim.time()] = Sim.time();
	    	  timeInStationArray[1][(int) Sim.time()] = leavingCar.leaveTime - leavingCar.enterTime;
	    	  
	    	  //Resource available, call enter event
	    	  new LeaveQueue().schedule (0.0);
	      }
	 }
	 
	 //The end
	 class EndOfSim extends Event {
	      public void actions() {
	         Sim.stop();
	      }
	 }
	 
	 
	 ///PROC
	 
	 //Car generation 
	 //Car comes regularly for the time being
	 //Not used, just for test
	 class  CarGen extends SimProcess {         
	       public void actions() {
	    	   double delay = 200;
	           while (true){
	              delay (delay);
	              new Arrival().schedule (0.0);
	           }
	       }   
	 }
	 
	 
	 //Simulation initialization.	 
	 public void simulate () {
	      Sim.init();
	      
	      //Basic event
	      new EndOfSim().schedule(duration);
	      //Starting generation proc
	     // new CarGen().schedule(0.0);
	      new Arrival().schedule (0.0);
	      
	      //GO
	      Sim.start();
	 }
	 
	 //Constructor
	 public GasStation( int a, int pn, double d, double min_dur_serv, double max_dur_serv)
	 {
		 wait_queue_size = a;
		 pump_nbr = pn;
		 pumps = new Resource[pump_nbr];
		 for (int i=0; i<pump_nbr; i++) 
		 {
		 	pumps[i] = new Resource (1, "A pump");
		 	pumps[i].setStatCollecting (true);
		 }
		 
		 usingPump = new Car[pump_nbr];
		 duration = d;
		 
		 clientNbr = 0;
		 clientMissedNbr = 0;
		 
		 min_time_refuel = min_dur_serv;
		 max_time_refuel = max_dur_serv;
		 
		 //Initialize array
		 //For each minute there is value linked
		 //We used a double array to ignored missing value
		 
		 nbClientArray = new double[2][(int) d];
		 
		 nbClientMissedArray = new double[2][(int) d];
		 
		 waitingQueueArray = new double[2][(int) d];
		 
		 timeInStationArray = new double[2][(int) d];
		 
		 waitingQueueSizeArray = new double[2][(int) d];
		 
		 pumpInUsageArray = new double[2][(int) d];
	 }
	 
	 ///Main
	 public static void main (String[] args) { 
		 
		  //LAUNCHING
		  GasStation GS = new GasStation(12, 4, 1440, 5.0, 15.0);
	      GS.simulate();
	      
	      //Reports
	      System.out.println (GS.NbClientsAcc.report());
	      System.out.println (GS.NbClientsMissedAcc.report());
	      System.out.println (GS.waitingQueueTimeTally.report());
	      System.out.println (GS.servingTimeTally.report());
	      
	      //Graph
	      XYLineChart chartNbClient  = new XYLineChart("Nombre total de client", "time", "Client nbr", nbClientArray);
	      chartNbClient.view(500, 500);
	      
	      XYLineChart chartMissedClient  = new XYLineChart("Nombre total de client loupés", "time", "Client nbr", nbClientMissedArray);
	      chartMissedClient.view(500, 500);
	      
	      XYLineChart waitingTime  = new XYLineChart("Temps d'attente", "time", "Waiting duration (minutes)", waitingQueueArray);
	      waitingTime.view(500, 500);
	   
	      XYLineChart servingTime  = new XYLineChart("Temps passé dans la station", "time", "Duration (minutes)", timeInStationArray);
	      servingTime.view(500, 500);
	      	      
	      XYLineChart queueSizeChart  = new XYLineChart("Taille de la file d'attente", "time", "Taille de la file d'attente", waitingQueueSizeArray);
	      queueSizeChart.view(500, 500);
	      
	      XYLineChart pumpUsed  = new XYLineChart("Nombre de pompes utilisés", "time", "Taille de la file d'attente", pumpInUsageArray);
	      pumpUsed.view(500, 500);
	      
	      
	  }
}
